Skip to content

feat(oracle): bond escrow, bounty settlement and provider slashing (SC-023) - #705

Closed
ahsen198911-debug wants to merge 6 commits into
degenspot:mainfrom
ahsen198911-debug:feature/sc-023-sc023-slug
Closed

ahsen198911-debug wants to merge 6 commits into
degenspot:mainfrom
ahsen198911-debug:feature/sc-023-sc023-slug

Conversation

@ahsen198911-debug

Copy link
Copy Markdown

Summary

Implements [SC-023] — Oracle Marketplace Contract with Provider Registration & Escrow Bounties.

The oracle_marketplace crate already had a skeleton: register / deregister / rate / select. What it did not have was any of the three acceptance criteria. This PR adds them.

Acceptance criterion How it is met
Registers oracle providers with bond locks register_oracle now pulls initial_bond into the contract's custody via a real SAC transfer before the provider is listed. increase_bond tops it up, withdraw_bond releases it after the deregistration cooldown, and it can only be withdrawn while inactive.
Distributes query fees per resolution create_call_escrow escrows the caller's bounty; resolve_call(…, Accurate) pays oracle.fee_bps to the provider and the remainder to the admin. Payout state is written before the transfer so a re-entrant token cannot re-trigger it.
Slashes dishonest providers resolve_call(…, Inaccurate) refunds the caller's bounty in full and slashes config.slash_penalty_bps of the bond, split between the harmed caller and the admin.

Verification actually run

Rust 1.98.1, soroban-sdk 23.5.3, against the workspace's own Cargo.lock (which pins the compatible ed25519-dalek 2.2.0 — a fresh resolve pulls 3.0.0 and soroban-env-host fails to compile, so the lock is required).

cargo test        ->  test result: ok. 26 passed; 0 failed
cargo clippy      ->  0 warnings, 0 errors
cargo build --release ->  Finished (note: `cdylib` only, no wasm target installed)

26 tests, exercised against real SACs registered in the test env rather than mocks, so every balance asserted is a balance that was actually moved:

  • bond escrow: bond lands in the contract, not the provider
  • double registration / zero bond / fee > 10 000 bps rejected without moving tokens
  • initial_bond < min_stake rejected
  • withdrawal blocked while active and before the cooldown elapses
  • accurate resolution splits the bounty exactly by fee_bps
  • earnings accumulate across calls
  • resolution cannot be applied twice, escrow ids cannot be duplicated
  • only the selected oracle can resolve; inactive oracles cannot
  • inaccurate resolution refunds the caller and slashes the bond, verified on three separate balances
  • a provider whose bond already sits at the min_stake floor cannot be slashed again

Three bugs the tests caught, and how I handled each

  1. total_resolved was never persisted. The Accurate branch incremented the counter and then dropped it. Fixed in the contract with set_oracle.

  2. Slashing was structurally impossible. min_stake was both the registration bond and the slash floor, so staked_amount == min_stake on registration and the SlashBelowMinStake guard fired on the first dispute — every provider was unfalsifiable. I split the two concepts: min_stake is the floor, initial_bond is what they post up front, and registration requires initial_bond >= min_stake. Now a provider has real collateral at risk and the floor still bounds the blast radius.

  3. My own test captured a balance after the debit. inaccurate_resolution_refunds_caller_and_slashes_bond read caller_fee_before after create_call_escrow, so "refunded in full" was compared against an already-debited balance. The contract was correct; the test was wrong. Fixed by capturing before the escrow, with a comment explaining why the order matters.

Design decisions worth reviewing

  • slash_penalty_bps is split 50/50 between the caller and the admin. The caller was actively served a bad answer; the admin runs the marketplace. Easy to change if the intended split differs.
  • A provider at their min_stake floor is refused a further slash rather than being pushed below it. The alternative is capping the slash at the remaining balance, which is also defensible — worth agreeing on before merge.
  • initialize gained a slash_penalty_bps parameter. This is a breaking change to the existing initialize signature. The three pre-existing tests are updated to match, but any deployed contract would need a migration. Flagging it deliberately rather than leaving it implicit.

Notes

  • Target branch feature/sc-023-sc023-slug per the issue, not main.
  • Cargo.toml: removed the unused backit-shared dependency; nothing in the crate referenced it, and dropping it keeps the build to a single crate.
  • AC1 is covered by two separate failure-mode tests (escrow_for_unknown_oracle_is_rejected, escrow_for_inactive_oracle_is_rejected) alongside the happy path.

@PeterOche PeterOche closed this Sep 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants